The Developer Fastlane

« 365 days to become a developer » challenge

PHP: Database gets & calls (PDO)

December 1, 2020

Summury

Exercise: Get data from database

Database creation

Made with with DBbrowser (SQLite)

Code (OOP)

Code
<?php

// Logical part

    try {
        $error = null;
        $dsn = __DIR__ . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR . 'data.db';
        $pdo = new PDO(
            "sqlite:$dsn", null, null, [ 
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // To be able to catch method-level exceptions
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ // To get an object from the fetch operation (instead of the default array)
        ]);
        try {
            $query = $pdo->query('SELECT * FROM posts ORDER BY date_creation DESC');
            $posts_obj = $query->fetchAll(); // Various fetch operations can be applied here: check lesson to get a list
        } catch (Exception $e) { // Catch exceptions thrown from the method ("query") level
            $error = '<b>Error message:</b> ' . $e->getMessage();
        }
    } catch (PDOException $e) { // Catch exceptions thrown from the new PDO instantiation level
        $error = 'Error message: ' . $e->getMessage();;
    }

// Displaying part ?>

    <?php if ($error): ?>
        <div class="alert alert-danger"><?= $error?></div>
    <?php else: ?>
        <p>Latest posts:</p>
        <ul>
            <?php foreach ($posts_obj as $post): ?>
                <?php $date = new DateTime("@{$post->date_creation}"); ?>
                <li><?= '<b>' . $post->title . '</b>' .  
                ' on ' . $date->format('M d, Y (H:i)') . 
                '<div style="font-size:11px;line-height:15px;margin-bottom:5px">' . 
                    $post->content . 
                '</div>' ?></li>
            <?php endforeach; ?>
        </ul>
    <?php endif; ?>

<?php // End of code

Result

With no error
Result

Latest posts:

  • Test title on Dec 01, 2020 (18:30)
    Test content
  • Lorem Ipsum on Nov 07, 2020 (19:19)
    This is some dumb content for post 2, for testing purpose!
  • This is a blog post title on Oct 01, 2020 (19:19)
    Lorem ipsum dolor, sit amet consectetur adipisicing elit. Incidunt ut, aperiam ea ducimus iste est nihil corporis repudiandae doloribus maiores quos! Necessitatibus alias ad odit quam sed voluptas voluptatibus vel.
Database connexion error
(Exception thrown from the PDO instantiation level)
Result
Error message: SQLSTATE[HY000] [14] unable to open database file
With query error
(Exception thrown from the query level: from any method applied to the new PDO instance)
Result
Error message: SQLSTATE[HY000]: General error: 1 near "SELECTdfgh": syntax error

LESSON

Database types:
  • SQLlite: use DB Browser software (for command lines only: SQLite extension for VisualStudioCode) + create a blank .db file (example: data.db)
  • MySQL: use phpMyAdmin (see lesson SQL: create database & tables

1. Connect to an SQL database with PHP

We create a new instantiation of the PDO class.
(PHP Reference: PDO > PDO::__construct)

SQLite
Code
new PDO(‘sqlite:../data.db’);
MySQL
Code
<?php
/* Connect to a MySQL database using driver invocation */
$dsn = 'mysql:dbname=testdb;host=127.0.0.1';
$user = 'dbuser';
$password = 'dbpass';

try { 
    $pdo = new PDO($dsn, $user, $password);
} catch (PDOException $e) { // Use a try/catch to manage exceptions
    echo 'Connection failed: ' . $e->getMessage();
}
?>

2. Create an SQL query

This is done via a method. Here are the most frequently used:
  • PDO::query — Executes an SQL statement, returning a result set as a PDOStatement object, or false in case of an error. (prefect for SELECT queries)
  • PDO::exec — Execute an SQL statement and return the number of affected rows (attention: pas compatible avec les requêtes SELECT)
  • PDO::prepare — Allows you to launch a "prepared" query / Prepares a statement for execution and returns a statement object
Code
$pdo = new PDO('sqlite:../data.db');
$query = $pdo->query('SELECT * FROM posts');
if ($query === false) {
    var_dump($pdo->errorInfo()); // Display error message during the development stage
    // do more stuff
}

3. Get and send data: the PDOStatement class

A PDOStatement new object is returned by the PDO::query method, among others.

Methods

PHP Reference > PDO > PDOStatement

Most frequently used:
Code
$dsn = __DIR__ . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR . 'data.db';       
try { 
    $pdo = new PDO("sqlite:$dsn");
    $query = $pdo->query('SELECT * FROM posts');
    if ($query === false) { // PDOStatement object returns "false" in case of error
        echo '<b>Query Error:</b><pre>'; print_r($pdo->errorInfo()); echo '</pre>';
    } else {
        $posts = $query->fetchAll();
        echo '
'; var_dump($posts); echo '
'; // Display all the table content in default style } } catch (Exception $e) { // Catch an Exception in case of failure connecting to database echo '<b>Connection failed:</b> ' . $e->getMessage(); }
Result
array(3) {
  [0]=>
  array(8) {
    ["id"]=>
    string(1) "1"
    [0]=>
    string(1) "1"
    ["title"]=>
    string(25) "This is a blog post title"
    [1]=>
    string(25) "This is a blog post title"
    ["content"]=>
    string(214) "Lorem ipsum dolor, sit amet consectetur adipisicing elit. Incidunt ut, aperiam ea ducimus iste est nihil corporis repudiandae doloribus maiores quos! Necessitatibus alias ad odit quam sed voluptas voluptatibus vel."
    [2]=>
    string(214) "Lorem ipsum dolor, sit amet consectetur adipisicing elit. Incidunt ut, aperiam ea ducimus iste est nihil corporis repudiandae doloribus maiores quos! Necessitatibus alias ad odit quam sed voluptas voluptatibus vel."
    ["date_creation"]=>
    string(10) "1601579987"
    [3]=>
    string(10) "1601579987"
  }
  [1]=>
  array(8) {
    ["id"]=>
    string(1) "2"
    [0]=>
    string(1) "2"
    ["title"]=>
    string(11) "Lorem Ipsum"
    [1]=>
    string(11) "Lorem Ipsum"
    ["content"]=>
    string(58) "This is some dumb content for post 2, for testing purpose!"
    [2]=>
    string(58) "This is some dumb content for post 2, for testing purpose!"
    ["date_creation"]=>
    string(10) "1604776787"
    [3]=>
    string(10) "1604776787"
  }
  [2]=>
  array(8) {
    ["id"]=>
    string(1) "3"
    [0]=>
    string(1) "3"
    ["title"]=>
    string(10) "Test title"
    [1]=>
    string(10) "Test title"
    ["content"]=>
    string(12) "Test content"
    [2]=>
    string(12) "Test content"
    ["date_creation"]=>
    string(10) "1606847434"
    [3]=>
    string(10) "1606847434"
  }
}

fetch_style parameters

PHP Reference > PDO > PDOStatement > PDOStatement::fetch

Most frequently used:
  • Returns an ARRAY:
    • PDO_FETCH_BOTH (default): returns an array indexed by both column name and 0-indexed column number as returned in your result set
    • PDO_FETCH_ASSOC (most used for procedural): returns an array indexed by column name as returned in your result set
    • PDO_FETCH_NUM: returns an array indexed by column number as returned in your result set, starting at column 0
  • Returns an OBJECT:
    • PDO_FETCH_OBJ (most used for OOP): returns an anonymous object with property names that correspond to the column names returned in your result set (a new instance of the stdClass built-in class is created)
    • PDO_FETCH_CLASS : returns a new instance of the requested class, mapping the columns of the result set to named properties in the class, and calling the constructor afterwards, unless PDO::FETCH_PROPS_LATE is also given.
    • PDO_FETCH_INTO: : updates an existing instance of the requested class, mapping the columns of the result set to named properties in the class
Code
$dsn = __DIR__ . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR . 'data.db';       
try { 
    $pdo = new PDO("sqlite:$dsn");
    // Return last posts on top of list: 
    $query = $pdo->query('SELECT * FROM posts ORDER BY date_creation DESC'); 
    if ($query === false) {
        echo '<b>Query Error:</b><pre>'; print_r($pdo->errorInfo()); echo '</pre>';
    } else {
        // Return database content into an object: 
        $posts_obj = $query->fetchAll(PDO::FETCH_OBJ);
        ?>
        Latest posts:
        <ul>
            // Go throught each post one by one to extract specific content:
            <?php foreach ($posts_obj as $post): ?>
                // Then display each element separatly:
                <?php $date = new DateTime("@{$post->date_creation}"); ?>
                <li>
                    <?= '<b>' . $post->title . '</b>' .  
                    ' on ' . $date->format('M d, Y (H:i)') . 
                    '<div style="font-size:11px;line-height:15px;margin-bottom: 5px">' . 
                        $post->content . 
                    '</div>' ?>
                </li>
            <?php endforeach; ?>
        </ul><?php
    }
} catch (Exception $e) {
    echo '<b>Connection failed:</b> ' . $e->getMessage();
}
Result
Connection failed: SQLSTATE[HY000]: General error: 1 near "SELECTdf": syntax error

4. Exceptions management (the PDO::setAttribute method)

Here is the issue about PDO and Exceptions:
  • PDO::__construct() throws a PDOException if the attempt to connect to the requested database fails.
  • On the other hand, if a method fails (for example: $pdo->query) as in the previous code, the method returns false. This return does not support exceptions by default...
  • To get around this problem, there are 2 solutions:
    • Use the PDO::setAttribute method to make errors returned as new PDOException instead of just returning a "false" statement:
      Code
      // PDO::setAttribute method's signature from PHP.net:
      
      public PDO::setAttribute ( int $attribute , mixed $value ) : bool
      
      // Application example:
      
      $pdo = new PDO("mysql:host=$host; dbname=$dbname", $db_username, $db_password);
      $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
      $query = $pdo->query('SELECT * FROM posts ORDER BY date_creation DESC'); 
          // Will return an Exception instead of a fatal error in case of failure
    • While creating a new instanciation of PDO class: set a 4th parameter (the PDO class's constructor allows a array of options as a 4th parameter). Those options correspond to the PDO::setAttribute methode described just before.
      Code
      // PDO class's signature from PHP.net:
      
      public PDO::__construct ( string $dsn [, string $username [, string $passwd [, array $options ]]] )
      
      // Application exemple:
      
      try {
          // Then pass the options as the last parameter in the connection string
          $pdo = new PDO(
              "mysql:host=$host; dbname=$dbname", 
              $db_username, 
              $db_password, 
              [ // Here can be set as many attributes as needed into an associative array
                  PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // That's how to set a failed result as a nex Exception
                  PDO::ATTR_CASE => PDO::CASE_NATURAL,
                  PDO::ATTR_ORACLE_NULLS => PDO::NULL_EMPTY_STRING
              ]
          $query = $pdo->query('SELECT * FROM posts ORDER BY date_creation DESC'); // Will return an Exception instead of a fatal error in case of failure
          );
      } catch(PDOException $e) {
          die("Database connection failed: " . $e->getMessage());
      }

5. Security notice

  1. Using htmlentities() function:
    To prevent users to put manually html signs inside the URL that may make the page code crash
    Code
    // Don't do this:
    
    <ul>
        <?php foreach($posts as $post): ?>
            <li><a href="/blog/edit.php?=id=<?= $post->id ?>"><?= $post->name ?></a></li>
        <?php endforeach; ?>
    </ul>
    
    // Do this instead:
    
    <ul>
        <?php foreach($posts as $post): ?>
            <li><a href="/blog/edit.php?=id=<?= $post->id ?>"><?= htmlentities($post->name) ?></a></li>
        <?php endforeach; ?>
    </ul>
  2. Using quote() function on a $_GET call:
    While getting data from the database throught a $_GET call, using the following syntax is dangerous, because it allows a hacker to make so-called "SQL injections":
    Code
    // Don't do this:
    
    $pdo = new PDO( ... );
    try {
        $query = $pdo->query('SELECT * FROM posts WHERE id = ' . $_GET['id']);
        $posts_obj = $query->fetchAll();
    } catch (Exception $e) { ... }
    
    // Do this instead:
    
    $pdo = new PDO( ... );
    $id = $pdo->quote($_GET['id']);
    try {
        $query = $pdo->query('SELECT * FROM posts WHERE id = ' . $id);
        $posts_obj = $query->fetchAll();
    } catch (Exception $e) { ... }
  3. The other solution is to use a PDO::prepare + PDOStatement::execute methods:
    Code
    $pdo = new PDO( ... );
    // step 1: prepare query with a undefined variable
    $query = $pdo->prepare('SELECT * FROM posts WHERE id = :id');
    // step 2: define variable and execute
    $query->execute([
        'id' => $_GET['id']
    ]);
    $post = $query->fetch();

6. Make several queries at once (transactions)

Transactions must be used each time we have more than 1 query from or to the database.

Methods to use:
Code
$pdo = new PDO("sqlite:$dsn", null, null, [ 
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ
]);
try {
    $pdo->beginTransaction();
        $pdo->exec('UPDATE posts SET name = "Test title" WHERE id = 3');
        $pdo->exec('UPDATE posts SET content = "Test content" WHERE id = 3');
        $post = $pdo->query('SELECT * FROM posts WHERE id = 3')->fetch();
    $pdo->commit();
    echo '<b>Updated post data:</b>';
    var_dump($post);
} catch (Exception $e) {
    $pdo->rollback();
    echo '<b>No change has been made to database due to a processing error:</b><br>' .
        $e->getMessage();
}
Result
No change has been made to database due to a processing error:
SQLSTATE[HY000]: General error: 1 no such column: name
Now if we change "name" by "title" as it is in the table:
Result
Updated post data:
object(stdClass)#1 (4) {
  ["id"]=>
  string(1) "3"
  ["title"]=>
  string(10) "Test title"
  ["content"]=>
  string(12) "Test content"
  ["date_creation"]=>
  string(10) "1606847434"
}

7. A few more usefull methods

© 2020 - Edouard Proust | The Developer Fastlane